Skip to content

S05-06 高级特性-注解

[TOC]

概述

什么是注解

在 Java 中,注解(Annotation) 是从 Java 5 开始引入的一种元数据(Metadata)机制。你可以把它理解为代码里的“特殊标签”。

注解本身不会直接影响代码的执行逻辑,但它们可以被编译器、开发工具(如 IntelliJ IDEA)、或者在程序运行时(通过反射)读取,从而执行相应的额外处理。

注解的作用

注解的作用:

注解通常用于以下三种场景:

  • 给编译器提供信息: 编译器可以利用注解来检测错误或抑制警告(例如 @Override@SuppressWarnings)。
  • 编译时或部署时的处理: 软件工具可以处理注解信息来生成代码、XML 文件、脚本等(例如 Lombok 的 @Getter 或 Spring 的组件扫描)。
  • 运行时处理: 某些注解可以在程序运行时被检查,结合反射(Reflection) 机制动态改变代码的行为(例如 Spring 的 @Autowired 或自定义的权限校验注解)。

内置标准注解

@Override

很高兴你想要深入了解这个具体的注解!虽然 @Override 看起来非常简单,但它是 Java 编程中最基础、也是最不可或缺的“安全卫士”之一。

@Override 是 Java 内置的标准注解,用来明确表示一个方法重写(覆盖)了其父类或接口中的方法

它本身没有任何运行时的逻辑,纯粹是给编译器看的。以下是关于 @Override 的详细深度解析:

作用

为什么我们需要 @Override(核心作用):

虽然在 Java 中,只要子类方法的签名(方法名、参数列表)和父类完全一致,就自动构成了重写,你完全可以不写 @Override。但是,强烈建议只要是重写的方法,就一定要加上它。原因有三个:

  1. 强大的“防呆”机制(捕捉拼写或签名错误)

    这是 @Override 最大的价值。很多时候,我们以为自己重写了父类方法,但由于拼写错误参数类型写错,实际上变成了**重载(Overload)**或者定义了一个全新的方法。

    如果加上了 @Override,编译器就会去父类里找对应的方法。如果找不到,编译器会直接报错(Compilation Error),把 Bug 扼杀在摇篮里。

  2. 提高代码可读性

    当其他开发者(或几个月后的你自己)阅读代码时,看到 @Override 标签,就能立刻明白:“哦,这个方法不是这个类特有的,它的定义来自父类或接口。”这极大降低了理解代码结构的成本。

  3. 应对父类修改时的“重构安全”

    假设你的类继承了一个第三方库的父类并重写了 process() 方法。未来第三方库更新了,把父类的 process() 改成了 processData()

    • 如果没有加 @Override:你的子类里的 process() 会变成一个普通的方法,默默地躺在那里,不再参与原有逻辑,系统会在运行时出现诡异的 Bug
    • 如果加了 @Override:升级库后重新编译时,编译器会立刻报错提示“该方法没有重写任何父类方法”,强迫你及时更新代码。

经典错误示例对比

经典错误示例对比:

来看一个非常容易踩坑的例子:重写 Object 类的 equals 方法。

❌ 不使用 @Override(埋下隐患):

java
public class User {
    private String name;

    // 开发者以为重写了 equals 方法,但实际上参数写成了 User 类型
    // 真正的 equals 方法参数必须是 Object 类型!
    public boolean equals(User other) {
        return this.name.equals(other.name);
    }
}

结果:这段代码可以完美编译,但把它放进 HashSet 或使用 == 比较时,底层调用的仍然是 Object 默认的 equals(比较内存地址),导致业务逻辑完全崩塌。

✅ 使用 @Override(立刻报错):

java
public class User {
    private String name;

    // 加上注解后,编译器会立刻标红报错:
    // "Method does not override method from its superclass"
    @Override
    public boolean equals(User other) {
        return this.name.equals(other.name);
    }
}

有了编译器的报错提示,开发者就会立刻意识到参数写错了,从而修正为 public boolean equals(Object other)

扒开 @Override 的源码看本质

扒开 @Override 的源码看本质:

如果你在 IDE 中按住 Ctrl (或 Cmd) 点击 @Override,你会看到它的 Java 源码极其简单:

java
package java.lang;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

结合我们之前学过的元注解知识,这两行元注解说明了它的本质:

  • @Target(ElementType.METHOD):这说明 @Override 只能写在方法上。如果你试图把它加在类、字段或接口声明上,编译器会报错。
  • @Retention(RetentionPolicy.SOURCE):这说明它是一个源码级别的注解。它只在编译阶段起作用,编译器检查完没问题后,编译生成的 .class 字节码文件中会完全丢弃这个注解。它不会占用任何运行时内存,也不会影响程序运行效率。

使用时的小知识点

使用时的小知识点:

  • 接口的实现(Java 6+ 的变化):在 Java 5 刚引入 @Override 时,它只能用于重写**类(Class)的方法。从 Java 6 开始,它也可以(且建议)用于标记实现了接口(Interface)**的方法。
  • 不能用于静态方法:静态方法属于类,不能被多态重写(只能被隐藏)。因此,你不能在子类的静态方法上使用 @Override

总结来说,@Override 是一个零性能开销、却能避免无数低级 Bug 的绝佳工具。在日常开发中,请把它当成一种强制的代码规范。

接下来,你是想了解另一个常用的标准注解 @Deprecated (标记过时) 的正确使用姿势,还是想看看如何利用 @SuppressWarnings 来优雅地消除编译器的烦人警告呢?

@Deprecated

这是一个非常棒的选择!随着代码库的不断演进,@Deprecated 是我们在日常开发和维护框架时经常会打交道的一个注解。

如果说 @Override 是保护你的“安全卫士”,那么 @Deprecated 就是代码世界里的**“拆迁公告”“危险警告牌”**。

它的核心作用是:告诉开发者,某个类、方法或字段已经“过时”了,不建议继续使用。

以下是关于 @Deprecated 的深度解析:

作用

为什么会需要 @Deprecated:

软件是在不断进化的。昨天写出的完美代码,今天可能就有了更好的实现方式,或者发现了底层的设计缺陷。但是,你不能直接把旧代码删掉,因为可能有成百上千的地方(甚至外部用户的系统)正在调用它。直接删除会导致编译报错甚至系统崩溃。

因此,我们使用 @Deprecated 作为一种平滑过渡的手段。通常出于以下原因:

  • 有更好的替代方案: 性能更好、命名更规范的新方法已经写好了。
  • 存在安全隐患或 Bug: 原方法存在无法修复的设计缺陷。
  • 架构重构: 某个类或接口在未来的版本规划中将被彻底移除。

@Deprecated 的表现形式

@Deprecated 的表现形式:

当你把一个元素标记为 @Deprecated 后,主要会产生两个效果:

  1. 编译器警告: 当其他代码调用了这个被标记的元素时,编译器会发出警告(Warning),提醒开发者正在使用过时的 API。

  2. IDE 视觉提示: 现代的开发工具(如 IntelliJ IDEA 或 Eclipse)会用一根删除线划掉这段代码,比如:oldMethod(),视觉上非常醒目。

正确的食用姿势

正确的食用姿势(结合 Javadoc):

敲黑板:仅仅加上 @Deprecated 注解是远远不够的!

一个负责任的开发者在废弃一个方法时,必须告诉别人:为什么废弃?以及我该用什么来代替? 这需要结合 Javadoc 中的 @deprecated 标签(注意大小写)来配合使用。

标准示范:

java
public class DateUtils {

    /**
     * 获取当前日期的字符串格式。
     * * @deprecated 该方法存在线程安全问题。请使用 {@link #getLocalDateStr()} 替代。
     */
    @Deprecated
    public static String getCurrentDateStr() {
        // ... 旧的、可能存在线程安全问题的实现 (例如使用 SimpleDateFormat) ...
        return "2026-03-14";
    }

    // 这是推荐使用的新方法
    public static String getLocalDateStr() {
        // ... 现代的、线程安全的实现 (例如使用 java.time.LocalDate) ...
        return "2026-03-14";
    }
}

Java 9 的重大升级:支持参数

Java 9 的重大升级:支持参数:

在 Java 9 之前,@Deprecated 只是一个毫无参数的标记。从 Java 9 开始,官方为它增加了两个非常实用的属性:

  • since (String): 记录该元素是从哪一个版本开始被废弃的。
  • forRemoval (boolean): 这是一个极其强烈的警告。如果设为 true,表示这个元素在未来的版本中一定会被彻底删除。此时,编译器发出的不再是普通的警告,而是“终极警告”(Terminally Deprecated)。

Java 9+ 的进阶用法:

java
// 告诉大家:这个方法从 v2.5 版本开始废弃,并且在未来的版本中会被彻底删除!
@Deprecated(since = "2.5", forRemoval = true)
public void dangerousMethod() {
 // ...
}

扒开源码看本质

扒开源码看本质:

我们在 IDE 中点进 @Deprecated 的源码:

java
package java.lang;

import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
public @interface Deprecated {
    String since() default "";
    boolean forRemoval() default false;
}

注意看它的元注解:

  • @Target 非常宽泛: 它几乎可以贴在任何地方(类、方法、字段、参数、甚至是模块和包上)。
  • @Retention(RetentionPolicy.RUNTIME) 与只在编译期存在的 @Override 不同,@Deprecated 是被保留到运行期的。这意味着框架可以通过反射(Reflection)在程序运行时检查某个方法是否过时,从而做出动态的降级处理或日志记录。
  • @Documented 这个注解会被包含在生成的 API 文档(Javadoc)中,这是非常合理的设计。

总结

总结:

@Deprecated 是 Java 保证向后兼容性(Backward Compatibility)的基石之一。它体现了优雅的软件工程思想:不要粗暴地切断过去,而是温柔地指引未来。

现在你已经掌握了 @Override(防错)和 @Deprecated(过渡)。接下来,你想继续了解用来压制编译器警告的 @SuppressWarnings,还是想看看如何在 Spring/Spring Boot 中自定义一个注解并通过 AOP 切面来实现业务逻辑(比如实现一个 @CheckLogin 权限校验)呢?

@SuppressWarnings

这是一个非常自然的进阶!在了解了 @Override(安全卫士)和 @Deprecated(危险警告牌)之后,我们来聊聊 Java 中最具“霸道总裁”气质的标准注解——@SuppressWarnings

你可以把它理解为开发者对编译器下达的**“静音指令”“免责声明”**。它的核心含义是:“我知道这里有风险,但我心里有数,请你(编译器)闭嘴,不要再给我报警告了。”

以下是对 @SuppressWarnings 的深度解析:

作用

为什么我们需要 @SuppressWarnings:

在 Java 编译代码时,如果编译器发现了潜在的问题(比如使用了过时的 API、泛型类型不安全、变量声明了却没使用等),它会发出警告(Warning)

虽然警告不会像错误(Error)那样打断编译,但如果项目中充斥着成百上千的无用警告,真正危险的警告就会被淹没在噪音中

使用 @SuppressWarnings 可以帮我们保持编译输出的整洁(Zero Warnings)。当我们明确知道某段代码是安全的,但由于语言特性的限制无法消除编译器的担忧时,就可以用它来“压制”特定的警告。

常见的“静音咒语”

常见的“静音咒语”(警告类型):

@SuppressWarnings 需要传入一个或多个字符串参数,来指定你要压制哪种类型的警告。最常用的参数包括:

  • "unchecked":抑制未检查的类型转换警告。这在处理历史遗留代码或与不支持泛型的旧 API 交互时非常常见(例如把普通的 List 强转为 List<String>)。
  • "deprecation":抑制使用了被 @Deprecated 标记的元素时发出的警告。(当你明知道它过时了,但由于某种原因必须得用时)。
  • "unused":抑制变量、方法、参数等声明了但未被使用的警告。
  • "rawtypes":抑制使用了原生类型(没有指定泛型参数,如直接用 Map 而不是 Map<String, Object>)时的警告。
  • "all":终极杀器,抑制所有类型的警告(强烈不建议滥用)。

代码实战示例

场景一:压制单种警告

场景一:压制单种警告(泛型转换):

在下面这个例子中,由于我们从一个旧的、不支持泛型的方法中获取了一个 List,编译器会非常担心里面装的不是 String

java
import java.util.ArrayList;
import java.util.List;

public class SuppressDemo {

    // 告诉编译器:我知道这个转换有风险,但我保证没问题,别吵了
    @SuppressWarnings("unchecked")
    public void processLegacyList() {
        // legacyMethod() 返回的是一个没有泛型的 raw List
        List<String> list = (List<String>) legacyMethod();
        System.out.println(list.get(0));
    }

    public List legacyMethod() {
        List oldList = new ArrayList();
        oldList.add("Hello Legacy");
        return oldList;
    }
}
场景二:同时压制多种警告

场景二:同时压制多种警告:

如果你需要同时压制多种警告,可以传入一个数组形式的字符串:

java
@SuppressWarnings({"unchecked", "deprecation"})
public void doSomethingRisky() {
    // 既有未检查的泛型转换,又使用了过时的方法
}

扒开源码看本质

扒开源码看本质:

在 IDE 中点进 @SuppressWarnings 的源码:

java
package java.lang;

import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}

注意看它的两大特征:

  1. @Target 极其广泛:你可以把它贴在类、字段、方法、参数、甚至局部变量上。

  2. @Retention(RetentionPolicy.SOURCE)它仅仅存在于源码阶段! 编译器在编译生成 .class 字节码文件时,一旦完成了警告检查,就会把这个注解无情地丢弃。它绝对不会对运行时的性能产生一丁点影响。

使用的“黄金法则”

使用的“黄金法则”(最佳实践):

虽然 @SuppressWarnings 很好用,但千万不要把它当成掩盖烂代码的遮羞布。使用它时请遵循以下原则:

  1. 作用域越小越好:千万不要直接把 @SuppressWarnings 贴在整个类(Class)上!如果只有一个局部变量有问题,就把它贴在局部变量上。作用域过大会导致你意外屏蔽掉真正有用的警告。

  2. 写上注释说明原因:当你压制了一个警告,意味着你在做一件“非常规”的事情。最好在旁边留下一行注释,告诉后来的接手者(或者几个月后的你自己)为什么要压制它

    java
    // 因为历史遗留的第三方库不支持泛型,此处强转是安全的
    @SuppressWarnings("unchecked")
    List<User> users = (List<User>) oldLibrary.getUsers();

总结

总结:

到这里,Java 最核心的三大基础注解:

  • @Override(防错,编译期校验)

  • @Deprecated(过渡,运行时保留提示)

  • @SuppressWarnings(消音,源码期辅助)

    我们就全部拆解完毕了。

既然你已经掌握了 Java 标准注解的运作原理,接下来,你希望我为你演示如何在 Spring Boot 中通过“自定义注解 + AOP 面向切面编程”来实现一个实际的企业级功能(例如:只需加一个 @CheckLogin 注解就能自动完成登录校验)吗?

@FunctionalInterface

在了解了 @Override@Deprecated 等传统注解后,我们来聊聊 Java 8 引入的一个具有划时代意义的标准注解:@FunctionalInterface

如果说前面的注解是在修补和完善面向对象编程(OOP),那么 @FunctionalInterface 就是 Java 正式拥抱**函数式编程(Functional Programming)**的基石。它是连接 Java 接口和 Lambda 表达式的桥梁。

以下是对 @FunctionalInterface 的深度解析:

什么是“函数式接口”

什么是“函数式接口”:

在讲注解之前,必须先明确概念:什么是函数式接口(Functional Interface)?

在 Java 中,如果一个接口有且仅有一个抽象方法(Single Abstract Method,简称 SAM),那么这个接口就被称为函数式接口。

常见的经典函数式接口有:

  • Runnable(只有一个 run() 方法)
  • Callable(只有一个 call() 方法)
  • Comparator(只有一个 compare() 方法)

@FunctionalInterface 的核心作用

@FunctionalInterface 的核心作用:

@Override 非常类似,@FunctionalInterface 是写给编译器看的,它是一个“编译期检查”注解。

  • 它是可选的: 只要一个接口符合“只有一个抽象方法”的规则,它天然就是一个函数式接口,即使不加这个注解,你依然可以用 Lambda 表达式来实例化它。
  • 它的价值在于“防呆”和“契约”: 如果你在接口上打上了 @FunctionalInterface,编译器就会强制检查这个接口。如果里面有 0 个抽象方法,或者有 2 个及以上的抽象方法,编译器会直接报错

它的存在是为了防止团队协作时的“破坏”: 假设你写了一个函数式接口供大家用 Lambda 调用。几个月后,新来的同事觉得这个接口功能不够,随手又加了一个抽象方法进去。结果就是,全公司所有用到这个接口的 Lambda 表达式全部编译报错!有了 @FunctionalInterface,新同事在加方法的那一刻就会被编译器拦下。

核心规则与“特权”

核心规则与“特权”(面试常考):

虽然规定“只能有一个抽象方法”,但 Java 8 为了保证向后兼容性,给接口赋予了很多新特性。因此,在使用 @FunctionalInterface 时,有几个非常重要的“例外”规则:

① 默认方法(default methods)不计入配额

① 默认方法(default methods)不计入配额:

Java 8 允许在接口中写带有具体实现的 default 方法。这些方法不是抽象方法,因此你可以有任意多个 default 方法,完全不影响它作为函数式接口。

② 静态方法(static methods)不计入配额

② 静态方法(static methods)不计入配额:

同理,接口中的静态方法有自己的实现,也不算作抽象方法。

③ 重写 Object 类的公共方法不计入配额

③ 重写 Object 类的公共方法不计入配额:

如果接口声明了一个抽象方法,但它的签名和 java.lang.Object 类中的公共方法(如 equals, toString, hashCode)一模一样,那么这个方法不会被算作那“唯一的一个抽象方法”。因为任何实现该接口的类,最终都会从 Object 类继承这些方法的实现。

代码实战展示

代码实战展示:

让我们来看一个完全合法的、包含了各种元素的复杂函数式接口:

java
@FunctionalInterface
public interface MyStringProcessor {

    // 1. 唯一的抽象方法(这才是真正被 Lambda 表达式实现的方法)
    String process(String input);

    // 2. 默认方法(不算抽象方法,合法)
    default void printLength(String input) {
        System.out.println("长度是: " + input.length());
    }

    // 3. 静态方法(不算抽象方法,合法)
    static void sayHello() {
        System.out.println("Hello Functional Interface!");
    }

    // 4. 重写 Object 的 public 方法(不算那个唯一的抽象方法,合法)
    @Override
    boolean equals(Object obj);

    // 注意:如果你在这里再加一个 void doSomething(); 编译器立马报错!
}

如何结合 Lambda 表达式使用它?

由于 MyStringProcessor 是一个函数式接口,我们不需要写臃肿的匿名内部类,直接用 Lambda 搞定:

java
public class Main {
    public static void main(String[] args) {
        // 使用 Lambda 表达式实现那个唯一的抽象方法 process()
        // 这里的逻辑是:把字符串转成大写
        MyStringProcessor toUpperCaseProcessor = (str) -> str.toUpperCase();

        // 调用
        String result = toUpperCaseProcessor.process("hello gemini");
        System.out.println(result); // 输出: HELLO GEMINI
    }
}

扒开源码看本质

扒开源码看本质:

我们在 IDE 中点进 @FunctionalInterface 的源码:

java
package java.lang;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}
  • @Target(ElementType.TYPE):说明它只能贴在类、接口或枚举上(实际上它专属用于接口 interface,如果贴在类或枚举上,编译器会报错提示它不是接口)。
  • @Retention(RetentionPolicy.RUNTIME):它在运行时是保留的。这意味着 Java 的底层框架和反射机制可以在运行时确认一个接口是不是被设计为函数式接口的。

总结

总结:

  1. 本质: 标记一个接口是“只有一个抽象方法的接口”。

  2. 作用: 触发编译器的安全检查,防止接口被意外添加多余的抽象方法而破坏 Lambda 表达式的调用。

  3. 例外: default 方法、static 方法以及 Objectpublic 方法不影响函数式接口的判定。

既然提到了 @FunctionalInterface 和 Lambda 表达式,Java 8 官方其实在 java.util.function 包下为我们内置了极其丰富的现成函数式接口(比如 Predicate, Function, Consumer, Supplier)。

你希望我为你盘点一下这四大核心内置函数式接口的应用场景吗?还是想了解我们在实际项目中是如何利用 Lambda 和 Stream API 优雅地处理集合数据的?

元注解

如果你把普通的注解(如 @Override@Deprecated)比作贴在代码上的**“标签”,那么元注解(Meta-Annotation)就是“制造标签的模具”或者说是“贴在标签上的标签”**。

简单来说:元注解的作用,就是用来定义其他注解的行事规则。 当你需要自定义一个注解时,元注解是绝对绕不开的核心。

Java 在 java.lang.annotation 包下提供了 5 个标准的元注解(其中 4 个是 Java 5 引入的,1 个是 Java 8 补充的)。下面为你深度拆解它们:

@Target

核心元注解一:@Target(规定作用目标):

@Target 决定了你自定义的注解可以贴在哪些代码元素上(是只能贴在方法上,还是类上,还是变量上?)。如果你把一个规定只能用在方法上的注解贴在了类上,编译器就会报错。

它接收一个 ElementType 数组作为参数。常用的 ElementType 包括:

  • TYPE:可以作用于类、接口(包括注解类型)、枚举。
  • FIELD:可以作用于字段(属性)、枚举常量。
  • METHOD:可以作用于方法。
  • PARAMETER:可以作用于方法参数。
  • CONSTRUCTOR:可以作用于构造方法。
  • LOCAL_VARIABLE:可以作用于局部变量。
  • ANNOTATION_TYPE:可以作用于另一个注解上(即把它变成一个新的元注解)。

示例:

java
// 这个注解只能用在类/接口和方法上
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface MyAnnotation { }

@Retention

核心元注解二:@Retention(规定存活寿命):

这是最重要的一个元注解!它决定了你的注解能“活”多久。它接收一个 RetentionPolicy 枚举值。

Java 代码从编写到运行,要经过“源码 -> 字节码文件 (.class) -> JVM 内存”三个阶段。@Retention 对应的三个值正好映射这三个阶段:

  • SOURCE(源码级):注解只在 .java 源码中存在,编译成 .class 文件时就会被编译器直接丢弃。

  • 典型代表: @Override@SuppressWarnings。它们只是给编译器看的,编译完就没用了。

  • CLASS(字节码级,默认值):注解会被保留在 .class 文件中,但在程序运行时(类加载时),JVM 会将其丢弃。

  • 应用场景: 主要是给一些字节码操作工具(如 Lombok 自动生成 Getter/Setter,或在编译期生成代码的 APT 技术)使用的。如果你不写 @Retention,默认就是它。

  • RUNTIME(运行级):注解不仅保留在 .class 文件中,还会被 JVM 加载到内存里。程序在运行期间,可以通过反射(Reflection)随时读取这个注解的信息。

  • 典型代表: Spring 的 @Autowired@Controller,以及我们日常业务开发中 99% 的自定义注解(例如自定义权限校验、日志记录等)。

@Documented

辅助元注解三:@Documented(是否加入文档):

这是一个标记注解(没有属性)。

如果你的自定义注解加上了 @Documented,那么当开发者使用 javadoc 工具为代码生成 API 文档时,这个注解的信息也会被包含在文档中。这有助于提升代码的可读性和规范性。

@Inherited

辅助元注解四:@Inherited(是否允许子类继承):

这也是一个标记注解,但它有一个非常容易踩坑的限制条件

默认情况下,如果父类打了一个注解,子类是不会自动继承这个注解的。但如果你的自定义注解被打上了 @Inherited,情况就改变了:

  • 当一个类被 @Inherited 修饰的注解标记时,它的子类会自动拥有这个注解。
  • ⚠️ 避坑指南: @Inherited 只对类(Class)有效!它对接口(Interface)无效,对重写的方法也无效。如果你在接口上用了带有 @Inherited 的注解,实现类是拿不到它的。

@Repeatable

新增元注解五:@Repeatable(Java 8 引入,是否可重复):

在 Java 8 之前,同一个注解在同一个元素上只能贴一次。比如你想在一个方法上加两个相同的调度注解是不行的。

Java 8 引入了 @Repeatable,允许你在同一个地方多次使用同一个注解。

底层逻辑: 它其实是一个“语法糖”。你需要定义一个“容器注解”来装载这些重复的注解。

示例:

java
// 1. 定义容器注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Schedules {
    Schedule[] value(); // 里面装的是 Schedule 数组
}

// 2. 定义真正的注解,并指定它的容器是 Schedules
@Repeatable(Schedules.class)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Schedule {
    String time();
}

// 3. 使用时,可以多次贴 @Schedule
public class Task {
    @Schedule(time = "10:00")
    @Schedule(time = "12:00")
    public void run() { }
}

综合实战

综合实战:打造一个完美的企业级自定义注解:

把上面的知识结合起来,我们来定义一个企业中最常见的“防重复提交”注解:

java
import java.lang.annotation.*;

@Target(ElementType.METHOD)          // 1. 作用在方法上
@Retention(RetentionPolicy.RUNTIME)  // 2. 运行时有效(必须的,因为要用AOP拦截)
@Documented                          // 3. 生成 Javadoc
// 注意:@Inherited 通常不需要,防重提交只针对特定方法
public @interface PreventDuplicateSubmit {

    // 锁定时间(默认 5 秒)
    int lockTime() default 5;

    // 提示信息
    String message() default "请勿频繁点击";
}

总结

总结:

元注解是构建 Java 注解体系的基石。记住最核心的两点:

  1. @Target 圈定注解用在哪里。

  2. @Retention(RetentionPolicy.RUNTIME) 保证注解在运行时能被反射或 AOP 拦截到。

目前为止,你已经掌握了 Java 标准注解和元注解的全部理论知识。

理论必须结合实践才能发挥威力!接下来,你想看看如何利用反射机制写一段代码,把上面那个 @PreventDuplicateSubmit 注解里的 lockTimemessage 给提取出来吗?还是想直接进入高阶玩法,看看它如何在 Spring Boot 的 AOP(面向切面编程)中大显身手?

自定义注解

在实际的企业级开发中(尤其是使用 Spring Boot 等框架时),自定义注解可以说是“消除重复代码、提升开发效率”的终极武器。

要记住一个核心心法:注解本身只是一个“标签”或“数据载体”,它没有任何执行逻辑。要让注解发挥作用,必须配合“解析器”(通常是通过反射 Reflection 或 AOP 技术)来实现。

接下来,我将带你分 5 个步骤,从零开始打造并解析一个完整的自定义注解。

第一步:使用 @interface 声明注解

在 Java 中,创建注解非常简单,它看起来就像是在创建一个接口,只不过多了一个 @ 符号。

java
public @interface RequireRole {
    // 这是一个空的注解
}

第二步:贴上元注解(制定规则)

刚才创建的 @RequireRole 还是个“黑户”,我们需要用元注解来告诉编译器和 JVM 这个标签的“使用说明书”。

假设我们想做一个权限校验的注解,它只能放在方法上,并且要在程序运行时生效,以便我们进行拦截校验。

java
import java.lang.annotation.*;

@Target(ElementType.METHOD)          // 规则1:只能贴在方法上
@Retention(RetentionPolicy.RUNTIME)  // 规则2:保留到运行期(最重要!否则反射读不到)
@Documented                          // 规则3:生成 JavaDoc 时包含它
public @interface RequireRole {
}

第三步:定义注解的属性(携带数据)

注解可以携带参数。在注解内部定义参数的方式看起来像是在定义无参方法

关于属性的 4 条铁律:

  1. 数据类型受限:只能是基本数据类型(int, boolean 等)、StringClass、枚举(Enum)、注解类型,以及上述类型的数组。不能使用包装类(如 Integer)或复杂对象。

  2. 可以有默认值:使用 default 关键字指定默认值。

  3. 特殊的 value 属性:如果你的注解只有一个属性,强烈建议把它命名为 value。这样在使用时,可以直接写 @RequireRole("ADMIN"),而不需要写 @RequireRole(value = "ADMIN")

  4. 不能有参数:属性定义后不能带参数,比如 String value(String param); 是绝对错误的。

完善我们的注解:

java
import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequireRole {

 // 核心属性:需要的角色名称
 String value() default "USER";

 // 辅助属性:校验失败时的提示信息
 String failMessage() default "您没有权限执行此操作";
}

第四步:在代码中使用自定义注解

标签做好了,现在我们把它贴到业务类的方法上去。

java
public class UserService {

    // 使用默认值:默认需要 "USER" 角色
    @RequireRole
    public void viewProfile() {
        System.out.println("查看个人信息成功...");
    }

    // 指定 value 值:需要 "ADMIN" 角色才能删除用户
    // 如果有多个属性,需要明确写出属性名,例如:@RequireRole(value = "ADMIN", failMessage = "滚蛋,你不是管理员!")
    @RequireRole("ADMIN")
    public void deleteUser() {
        System.out.println("删除用户成功...");
    }
}

第五步:编写解析器(注入灵魂)

这是最关键的一步。如果不写解析器,那两个 @RequireRole 标签就只是一串毫无作用的死代码。由于我们将 @Retention 设置为了 RUNTIME,我们可以使用**反射(Reflection)**在运行时读取它。

下面我们写一个简单的模拟拦截器:

java
import java.lang.reflect.Method;

public class SecurityInterceptor {

    // 模拟当前登录用户的角色
    private static final String CURRENT_USER_ROLE = "USER";

    public static void main(String[] args) throws Exception {
        UserService userService = new UserService();
        // 获取 UserService 的 Class 对象
        Class<?> clazz = userService.getClass();

        // 遍历该类的所有方法
        for (Method method : clazz.getDeclaredMethods()) {

            // 核心 1:判断该方法上是否贴了 @RequireRole 注解
            if (method.isAnnotationPresent(RequireRole.class)) {

                // 核心 2:获取该方法上的注解实例
                RequireRole roleAnnotation = method.getAnnotation(RequireRole.class);

                // 核心 3:提取注解携带的数据
                String requiredRole = roleAnnotation.value();
                String failMsg = roleAnnotation.failMessage();

                System.out.println(">>> 正在拦截方法: " + method.getName());
                System.out.println("该方法需要角色: " + requiredRole);

                // 核心 4:执行我们的业务校验逻辑
                if (CURRENT_USER_ROLE.equals(requiredRole)) {
                    System.out.println("权限校验通过!放行...");
                    // 通过反射动态调用该方法
                    method.invoke(userService);
                } else {
                    System.err.println("拦截失败:" + failMsg);
                }
                System.out.println("-------------------------");
            }
        }
    }
}

运行结果:

你会看到 viewProfile 方法成功执行,而 deleteUser 方法由于当前用户只有 "USER" 角色,被成功拦截并打印了错误信息。

总结:

自定义注解的三步曲:

  1. 定义外壳@interface + 元注解(@Target, @Retention)。

  2. 定义属性:决定这个标签能存什么数据(善用 valuedefault)。

  3. 编写解析器:通过反射字节码技术读取注解数据,并执行真实逻辑。

在上面这个纯 Java 的例子里,我们必须自己写 for 循环去扫描方法。但在实际的 Spring Boot 项目中,Spring 容器帮我们把“扫描类和方法”的脏活累活全干了。

想要我为你演示,如何把今天学的自定义注解知识,直接搬进 Spring Boot 项目中,结合 AOP(面向切面编程)写一个真正能用在企业级项目里的 @CheckLogin@LogAction 注解吗? 这绝对是简历上的亮点技能!

分隔-----------------------------

注解的理解

  1. 注解(Annotation)又称元数据,用于修饰解释程序元素(包、类、方法等)
  2. 不影响程序逻辑,但可被编译或运行
  3. JavaSE 中用途:标记过时功能、忽略警告等
  4. JavaEE 中用途:配置应用、替代 XML 配置等

基本的 Annotation 介绍

使用 Annotation 时需加@符号,作为修饰符使用。

三个基本 Annotation

  1. @Override:限定方法重写父类方法,仅用于方法
  2. @Deprecated:标记程序元素已过时
  3. @SuppressWarnings:抑制编译器警告

基本的 Annotation 应用案例

@Override 注解(Override_.java)

java
package com.hspedu.annotation_;

public class Override_ {
    public static void main(String[] args) {
    }
}

class Father { // 父类
    public void fly() {
        System.out.println("Father fly...");
    }

    public void say() {}
}

class Son extends Father { // 子类
    // @Override 表示重写父类方法
    @Override
    public void fly() {
        System.out.println("Son fly....");
    }

    @Override
    public void say() {}
}

注意事项

  • 编译器会检查是否真的重写了父类方法
  • 仅能修饰方法
  • 源码:@Target(ElementType.METHOD)

@Deprecated 注解(Deprecated_.java)

java
package com.hspedu.annotation_;

public class Deprecated_ {
    public static void main(String[] args) {
        A a = new A();
        a.hi(); // 警告:方法已过时
        System.out.println(a.n1); // 警告:字段已过时
    }
}

@Deprecated // 类已过时
class A {
    @Deprecated // 字段已过时
    public int n1 = 10;

    @Deprecated // 方法已过时
    public void hi() {}
}

注意事项

  • 可修饰类、方法、字段、包、参数等
  • 用于版本升级过渡
  • 源码:@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})

@SuppressWarnings 注解(SuppressWarnings_.java)

java
package com.hspedu.annotation_;

import java.util.ArrayList;
import java.util.List;

// 抑制多种警告
@SuppressWarnings({"rawtypes", "unchecked", "unused"})
public class SuppressWarnings_ {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("jack");
        list.add("tom");
        int i; // 未使用但无警告
        System.out.println(list.get(1));
    }

    public void f1() {
        @SuppressWarnings({"rawtypes"})
        List list = new ArrayList();
        list.add("jack");
    }
}

常用抑制类型

  • all:抑制所有警告
  • rawtypes:抑制未指定泛型的警告
  • unchecked:抑制未检查的警告
  • unused:抑制未使用变量的警告

JDK 的元 Annotation(了解)

元 Annotation 用于修饰其他 Annotation,共 4 种:

元注解作用
Retention指定注解的作用范围(SOURCE/CLASS/RUNTIME)
Target指定注解可修饰的程序元素
Documented指定注解是否在 javadoc 中体现
Inherited子类继承父类的注解

@Retention

  • RetentionPolicy.SOURCE:编译器使用后丢弃(如@Override)
  • RetentionPolicy.CLASS:记录在 class 文件中,JVM 不保留(默认)
  • RetentionPolicy.RUNTIME:记录在 class 文件中,JVM 保留,可通过反射获取

@Target

指定注解可修饰的程序元素(如 METHOD、TYPE、FIELD 等)

第 10 章作业

代码执行结果题(Homework01.java)

java
class Car {
    static String color = "white";
    double price = 10;

    public Car() {
        this.price = 9;
        this.color = "red";
    }

    public Car(double price) {
        this.price = price;
    }

    public String toString() {
        return price + "\t" + color;
    }
}

public class Homework01 {
    public static void main(String[] args) {
        Car c1 = new Car(100);
        System.out.println(c1); // 100.0	red
        Car c = new Car();
        System.out.println(c); // 9.0	red
    }
}

编程题(Homework02.java)- 衣服序列号生成

java
package com.hspedu.homework;

public class Homework02 {
    public static void main(String[] args) {
        // 测试getNextNum方法
        System.out.println(Frock.getNextNum()); // 100100
        System.out.println(Frock.getNextNum()); // 100200

        // 测试三个Frock对象
        Frock f1 = new Frock();
        Frock f2 = new Frock();
        Frock f3 = new Frock();
        System.out.println(f1.getSerialNumber()); // 100300
        System.out.println(f2.getSerialNumber()); // 100400
        System.out.println(f3.getSerialNumber()); // 100500
    }
}

class Frock {
    // 私有静态属性:序列号起始值
    private static int currentNum = 100000;
    // 序列号属性
    private int serialNumber;

    // 构造器:获取唯一序列号
    public Frock() {
        this.serialNumber = getNextNum();
    }

    // 静态方法:生成下一个序列号
    public static int getNextNum() {
        currentNum += 100;
        return currentNum;
    }

    // get方法
    public int getSerialNumber() {
        return serialNumber;
    }
}

编程题(Homework03.java)- 抽象类使用

java
package com.hspedu.homework;

public class Homework03 {
    public static void main(String[] args) {
        Animal cat = new Cat();
        cat.shout(); // 猫会喵喵叫
        Animal dog = new Dog();
        dog.shout(); // 狗会汪汪叫
    }
}

// 抽象动物类
abstract class Animal {
    public abstract void shout();
}

// 猫类
class Cat extends Animal {
    @Override
    public void shout() {
        System.out.println("猫会喵喵叫");
    }
}

// 狗类
class Dog extends Animal {
    @Override
    public void shout() {
        System.out.println("狗会汪汪叫");
    }
}

编程题(Homework04.java)- 匿名内部类使用

java
package com.hspedu.homework;

public class Homework04 {
    public static void main(String[] args) {
        Cellphone cellphone = new Cellphone();
        // 测试计算功能:10+20
        cellphone.testWork(new Calculator() {
            @Override
            public double work(double n1, double n2) {
                return n1 + n2;
            }
        }, 10, 20);

        // 测试计算功能:50-30
        cellphone.testWork(new Calculator() {
            @Override
            public double work(double n1, double n2) {
                return n1 - n2;
            }
        }, 50, 30);
    }
}

// 计算器接口
interface Calculator {
    double work(double n1, double n2);
}

// 手机类
class Cellphone {
    // 测试计算功能
    public void testWork(Calculator calculator, double n1, double n2) {
        double result = calculator.work(n1, n2);
        System.out.println("计算结果:" + result);
    }
}

编程题(Homework05.java)- 局部内部类

java
package com.hspedu.homework;

public class Homework05 {
    public static void main(String[] args) {
        A a = new A();
        a.m1();
    }
}

class A {
    private String name = "外部类A的name";

    public void m1() {
        // 局部内部类B
        class B {
            private final String name = "局部内部类B的name";

            public void show() {
                // 打印内部类和外部类的name
                System.out.println("内部类name:" + name);
                System.out.println("外部类name:" + A.this.name);
            }
        }

        // 创建B对象并调用方法
        B b = new B();
        b.show();
    }
}

编程题(Homework06.java)- 工厂模式+接口

java
package com.hspedu.homework;

public class Homework06 {
    public static void main(String[] args) {
        // 实例化唐僧
        Person tangseng = new Person("唐僧", VehicleFactory.getHorse());
        tangseng.useVehicle(); // 使用马匹

        // 遇到大河,切换交通工具
        tangseng.setVehicles(VehicleFactory.getBoat());
        tangseng.useVehicle(); // 使用船只
    }
}

// 交通工具接口
interface Vehicles {
    void work();
}

// 马匹类
class Horse implements Vehicles {
    @Override
    public void work() {
        System.out.println("马匹奔跑...");
    }
}

// 船只类
class Boat implements Vehicles {
    @Override
    public void work() {
        System.out.println("船只航行...");
    }
}

// 交通工具工厂类
class VehicleFactory {
    // 获取马匹
    public static Vehicles getHorse() {
        return new Horse();
    }

    // 获取船只
    public static Vehicles getBoat() {
        return new Boat();
    }
}

// 人类
class Person {
    private String name;
    private Vehicles vehicles;

    // 构造器
    public Person(String name, Vehicles vehicles) {
        this.name = name;
        this.vehicles = vehicles;
    }

    // 使用交通工具
    public void useVehicle() {
        System.out.println(name + "使用");
        vehicles.work();
    }

    // setter方法
    public void setVehicles(Vehicles vehicles) {
        this.vehicles = vehicles;
    }
}

编程题(Homework07.java)- 成员内部类

java
package com.hspedu.homework;

public class Homework07 {
    public static void main(String[] args) {
        // 测试不同温度
        Car car1 = new Car(50);
        car1.getAir().flow(); // 吹冷气

        Car car2 = new Car(-5);
        car2.getAir().flow(); // 吹暖气

        Car car3 = new Car(25);
        car3.getAir().flow(); // 关闭空调
    }
}

class Car {
    private double temperature; // 温度
    // 成员内部类:空调
    class Air {
        public void flow() {
            if (temperature > 40) {
                System.out.println("温度超过40度,吹冷气");
            } else if (temperature < 0) {
                System.out.println("温度低于0度,吹暖气");
            } else {
                System.out.println("温度适宜,关闭空调");
            }
        }
    }

    // 构造器
    public Car(double temperature) {
        this.temperature = temperature;
    }

    // 获取空调对象
    public Air getAir() {
        return new Air();
    }
}

编程题(Homework08.java)- 枚举类

java
package com.hspedu.homework;

public class Homework08 {
    public static void main(String[] args) {
        // 测试show方法
        for (Color color : Color.values()) {
            color.show();
        }

        // switch中使用枚举
        Color color = Color.RED;
        switch (color) {
            case RED:
                System.out.println("选中红色");
                break;
            case BLUE:
                System.out.println("选中蓝色");
                break;
            case BLACK:
                System.out.println("选中黑色");
                break;
            case YELLOW:
                System.out.println("选中黄色");
                break;
            case GREEN:
                System.out.println("选中绿色");
                break;
        }
    }
}

// 接口
interface ShowColor {
    void show();
}

// 颜色枚举类
enum Color implements ShowColor {
    // 枚举对象及属性值
    RED(255, 0, 0),
    BLUE(0, 0, 255),
    BLACK(0, 0, 0),
    YELLOW(255, 255, 0),
    GREEN(0, 255, 0);

    // 属性
    private int redValue;
    private int greenValue;
    private int blueValue;

    // 构造器
    Color(int redValue, int greenValue, int blueValue) {
        this.redValue = redValue;
        this.greenValue = greenValue;
        this.blueValue = blueValue;
    }

    // 实现接口方法
    @Override
    public void show() {
        System.out.println("颜色RGB值: " + redValue + "," + greenValue + "," + blueValue);
    }
}